<?php

/* $Id: LSCache.php 5 2012-04-15 12:38:03Z lukas.slansky@gmail.com $ */

/**
 * Simple cache that saves its data into files.
 * @author Lukas Slansky <Lukas.Slansky@gmail.com>
 * @version 1.0
 * 
 * Example of usage:
 * <code>
 * <?php
 * $cache = new LSCache('testCache', dirname('/tmp/cache'));
 * $test = $cache->get('testKey');
 * if ($test) {
 *   echo "Got cached value: " . $test->getValue();
 *   if ($test->isExpired()) {
 *     echo "...however it is expired\n";
 *   } else {
 *     echo "...and it is valid\n";
 *     exit;
 *   }
 * }
 * echo "Setting new value\n";
 * $cache->put('testKey', 'testValue');
 * ?>
 * </code>
 */
class LSCache {

    private $_cacheName;
    private $_cacheDir;
    private $_defaultTtl;

    const FILE_PREFIX = 'LS_CACHE_';

    /**
     * Constructs new cache object
     * @param string $cacheName Name of the cache
     * @param string $cacheDir Directory (folder) to save cached data
     * @param int $ttl Time to live in seconds. Defaults to 60
     * @throws LSCacheException on error creating cache
     */
    public function __construct($cacheName, $cacheDir, $ttl = 60) {
        if (!is_dir($cacheDir)) {
            throw new LSCacheException("Error creating cache - cache dir does not exist");
        }
        $this->_cacheName = $cacheName;
        $this->_cacheDir = $cacheDir;
        $this->_defaultTtl = $ttl;
    }

    /**
     * Puts value into cache.
     * @param string $key Key to reference the value
     * @param string $value Value to put into cache
     * @param int $ttl Time to live for value. Defaults to cache's TTL.
     * @throws LSCacheException on error putting file into disk cache
     */
    public function put($key, $value, $ttl = null) {
        $entity = new LSCacheEntity($key, $value, $ttl == null ? $this->_defaultTtl : $ttl);
        $filename = $this->_fileName($key);
        $toWrite = serialize($entity);
        $lockFile = $this->_fileLock($filename . '.lock', LOCK_EX);
        $file = fopen($filename, "w");
        if (!$file) {
            $this->_fileUnlock($lockFile);
            throw new LSCacheException("Error openning cache file");
        }
        fwrite($file, $toWrite);
        fflush($file);
        fclose($file);
        $this->_fileUnlock($lockFile);
    }

    /**
     * Puts value into cache.
     * @param string $key Key to reference the value
     * @returns LSCacheEntity cache entity or null when no entity found
     * @throws LSCacheException on error locking
     */
    public function get($key) {
        $filename = $this->_fileName($key);
        if (!is_readable($filename)) {
            return null;
        }
        $lockFile = $this->_fileLock($filename . '.lock');
        $file = fopen($filename, "r");
        if (!$file) {
            $this->_fileUnlock($lockFile);
            return null;
        }
        if (!flock($file, LOCK_SH)) {
            $this->_fileUnlock($lockFile);
            throw new LSCacheException("Error getting lock");
        }
        $serialized = fread($file, filesize($filename));
        fclose($file);
        $this->_fileUnlock($lockFile);
        if (!$serialized) {
            return null;
        }
        return unserialize($serialized);
    }

    /**
     * Flushes cache from disk.
     */
    public function flush() {
        $dir = opendir($this->_cacheDir);
        while ($f = readdir($dir)) {
            if (strstr($f, LSCache::FILE_PREFIX . $this->_cacheName)) {
                unlink($this->_cacheDir . DIRECTORY_SEPARATOR . $f);
            }
        }
        closedir($dir);
    }

    /**
     * Generates value key hash.
     * @param string $key Key to get hash
     * @return type string hashed key
     */
    protected function _valueKey($key) {
        return md5($this->_cacheName . $key);
    }

    /**
     * Generates filename to key.
     * @param type $key key to generate file
     * @return string filename of the file
     */
    protected function _fileName($key) {
        return $this->_cacheDir . DIRECTORY_SEPARATOR . LSCache::FILE_PREFIX . $this->_cacheName . $this->_valueKey($key);
    }

    /**
     * Locks the file
     * @param string $filename Filename of the lock
     * @param int $lockType The type of the lock
     * @return resource The lock file handle
     * @throws LSCacheException on error getting lock
     */
    private function _fileLock($filename, $lockType = LOCK_SH) {
        $lockFile = fopen($filename . '.lock', "w");
        if (!flock($lockFile, $lockType)) {
            throw new LSCacheException("Error getting lock");
        }
        return $lockFile;
    }

    /**
     * Unlocks the file
     * @param resource $lockFile The lock file handle
     */
    private function _fileUnlock($lockFile) {
        flock($lockFile, LOCK_UN);
        fclose($lockFile);
    }
}

/**
 * Cache entity. 
 */
class LSCacheEntity {

    private $_created;
    private $_expires;
    private $_key;
    private $_value;

    /**
     * Constructs Cache entity.
     * @param string $key The key
     * @param Mixed $value The value
     * @param int $ttl Time to live for the value
     */
    public function __construct($key, $value, $ttl) {
        $this->_key = $key;
        $this->_value = $value;
        $this->_created = time();
        $this->_expires = $this->_created + $ttl;
    }

    /**
     * Returns entity expiration.
     * @return boolean Is entity expired?
     */
    public function isExpired() {
        return $this->_expires < time();
    }

    /**
     * Returns cache entity value.
     * @return Mixed The value
     */
    public function getValue() {
        return $this->_value;
    }

    /**
     * Returns cache entity key.
     * @return string The key
     */
    public function getKey() {
        return $this->_key;
    }

    /**
     * Returns cache entity creation timestamp.
     * @return int The timestamp.
     */
    public function getCreated() {
        return $this->_created;
    }

    /**
     * Returns cache entity expiration timestamp.
     * @return int The timestamp.
     */
    public function getExpires() {
        return $this->_expires;
    }

}

/**
 * The Cache Exception
 */
class LSCacheException extends Exception {
    
}
